﻿using System;
using System.Threading;
using QuickFix;
using PSE.Communication.Fix.Common;
using System.Collections.Generic;
using System.Xml;
using System.Configuration;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using PSE.Framework.Cryptography;
using System.Linq;
using System.Diagnostics;

//TODO: Trocar os numeros "magicos" no código por constantes
namespace PSE.Communication.Fix.Server.Session
{
    public class Session : QuickFix.Application, IDisposable
    {
        #region Attributes

        private SocketInitiator _socketInitiator;
        private SessionID _sessionID;
        private ManualResetEvent _logonDone;
        private ManualResetEvent _exitDeliveryBackupMessages;
        private ManualResetEvent _exitDeliveryRepositoryMessages;
        private FixSessionConfigurationElement _sessionInformation;
        private string _dataDictionary;
        private Queue<KeyValuePair<int, string>> _messagesNotDelivered;
        private Queue<RepositoryParameter> _messagesToDelivery;
        private Thread _threaDeliveryRepositoryMessages;
        private Thread _threadDeliveryBackupMessages;
        private FileRepository _fileRepository;
        private CommunicationState _state;
        private Random _randomizer;
        private string _pwd;
        private ManualResetEvent _normalMode;
        private string _internalError;

        #endregion

        #region Properties

        public SessionID SessionID
        {
            get { return _sessionID; }
            set { _sessionID = value; }
        }

        public CommunicationState State
        {
            get { return _state; }
            set { _state = value; }
        }

        #endregion

        #region Constructor

        public Session(FixSessionConfigurationElement sessionInformation, string dataDictionary)
        {
            _sessionInformation = sessionInformation;

            _logonDone = new ManualResetEvent(false);
            _exitDeliveryBackupMessages = new ManualResetEvent(false);
            _exitDeliveryRepositoryMessages = new ManualResetEvent(false);
            _normalMode = new ManualResetEvent(false);
            _normalMode.Set();

            _messagesNotDelivered = new Queue<KeyValuePair<int, string>>();
            _messagesToDelivery = new Queue<RepositoryParameter>();

            _threaDeliveryRepositoryMessages = new Thread(new ThreadStart(DeliveryRepositoryMessages));
            _threadDeliveryBackupMessages = new Thread(new ThreadStart(DeliveryBackupMessages));
            _randomizer = new Random();

            _state = CommunicationState.Created;
            _sessionInformation = sessionInformation;
            _dataDictionary = dataDictionary;

            _fileRepository = new FileRepository(sessionInformation.FileLogPath);

            SessionSettings sessionSettings = CreateSessionSettings();

            _socketInitiator = new SocketInitiator(this, new FileStoreFactory(sessionSettings), sessionSettings,
                new FileLogFactory(sessionSettings), new DefaultMessageFactory());

            _state = CommunicationState.Opening;

            _socketInitiator.start();

            _logonDone.WaitOne(20000);//TODO: 6 seg sempre sera suficiente ????

            if (_state != CommunicationState.Opened)
            {
                Dispose();

                throw new Exception(string.Format("Cannot establish connection with session Id '{0}' in the Fix Gateway acceptor {1}. {2}", _sessionID.ToString(), _sessionInformation.SocketConnectHost, !string.IsNullOrEmpty(_internalError) ? _internalError : string.Empty));
            }

            _threaDeliveryRepositoryMessages.Start();
            _threadDeliveryBackupMessages.Start();
        }

        #endregion

        #region FixEvents

        public void fromAdmin(QuickFix.Message message, QuickFix.SessionID sessionID)
        {
            string text = String.Empty;
            string errorNumber = String.Empty;

            if (message.isSetField(58))  // Text
            {
                text = message.getField(58);
                _internalError = text;
            }
            if (message.isSetField(925)) // NewPassword
            {
                ReplacePassword();
            }
            
            if (!String.IsNullOrEmpty(text) && text.Contains(':'))
                errorNumber = text.Split(':')[1].Trim();
            
            switch (errorNumber)
            {
                case "10005": // Senha expirou
                    {
                        GenerateNewPassword();
                        break;
                    }
                case "10008":
                    {
                        ReplacePassword(); // Senha atual nao confere
                        break;
                    }
            }
        }

        public void fromApp(QuickFix.Message message, QuickFix.SessionID sessionID)
        {
            MsgType field = new MsgType();

            if (message.getHeader().isSetField(field))
            {
                message.getHeader().getField(field);

                switch (field.getValue())
                {
                    //case "8": // Execution Report
                    case MessageType.ExecutionReport:
                        SaveMessage((QuickFix44.ExecutionReport)message, sessionID);
                        break;
                }
            }
        }

        public void onCreate(QuickFix.SessionID sessionID)
        {
            QuickFix.Session session = QuickFix.Session.lookupSession(sessionID);

            IRepositoryProvider repository = RepositoryManager.GetRepositoryProvider();

            int companyId = _sessionInformation.Brokerage;
            int num = repository.GetLastMessageNum(companyId, MessageType.ExecutionReport);

            session.setNextTargetMsgSeqNum(num);
        }

        public void onLogon(QuickFix.SessionID sessionID)
        {
            _state = CommunicationState.Opened;

            _logonDone.Set();
        }

        public void onLogout(QuickFix.SessionID sessionID)
        {
            _state = CommunicationState.Closed;
        }

        public void toAdmin(QuickFix.Message message, QuickFix.SessionID sessionID)
        {
            if (IsMessageOfType(message, MsgType.LOGON))
            {
                AddLogonField(message);
            }
        }

        public void toApp(QuickFix.Message message, QuickFix.SessionID sessionID)
        {
        }

        #endregion

        #region CreateSessionSettings

        private SessionSettings CreateSessionSettings()
        {
            SessionSettings sessionSettings = new SessionSettings();

            Dictionary dicDefault = new Dictionary("DEFAULT");
            dicDefault.setString("FileLogPath", _sessionInformation.FileLogPath);
            sessionSettings.set(dicDefault);

            Dictionary dicSession = new Dictionary();
            dicSession.setString("ConnectionType", _sessionInformation.ConnectionType);
            dicSession.setString("FileStorePath", _sessionInformation.FileStorePath);
            dicSession.setString("SocketConnectHost", _sessionInformation.SocketConnectHost);
            dicSession.setLong("SocketConnectPort", _sessionInformation.SocketConnectPort);
            dicSession.setString("StartTime", _sessionInformation.StartTime);
            dicSession.setString("EndTime", _sessionInformation.EndTime);
            dicSession.setLong("HeartBtInt", _sessionInformation.HeartBtInt);
            dicSession.setString("DataDictionary", _dataDictionary);
            dicSession.setString("ResetOnLogon", _sessionInformation.ResetOnLogon);
            dicSession.setString("ValidateUserDefinedFields", _sessionInformation.ValidateUserDefinedFields);

            _sessionID = new SessionID(_sessionInformation.BeginString, _sessionInformation.SenderCompID, _sessionInformation.TargetCompID);

            sessionSettings.set(_sessionID, dicSession);

            return sessionSettings;
        }

        #endregion

        #region AddLogonField

        private void AddLogonField(QuickFix.Message message)
        {
            message.getHeader().setField(new SenderSubID(_sessionInformation.Brokerage.ToString()));

            if (_sessionInformation.ResetAll)
            {
                message.getHeader().setField(new RawDataLength(_sessionInformation.DefaultPassword.Length));
                message.getHeader().setField(new RawData(_sessionInformation.DefaultPassword));
            }
            else
            {
                string password = RijndaelCryptographer.DecryptString(_sessionInformation.Password);
                message.getHeader().setField(new RawData(password));
                message.getHeader().setField(new RawDataLength(password.Length));
            }
            if (!String.IsNullOrEmpty(_sessionInformation.NewPassword))
            {
                string newPassword = RijndaelCryptographer.DecryptString(_sessionInformation.NewPassword);
                message.setField(new NewPassword(newPassword));
            }
        }

        #endregion

        #region Password

        private void ReplacePassword()
        {
            XmlDocument doc = new XmlDocument();

            string path = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).FilePath;

            doc.Load(path);

            XmlNode node = doc.SelectSingleNode(string.Format("/configuration/fixConfiguration/fixSessions/session[@name='{0}']", _sessionInformation.Name));

            if (node.Attributes["password"] != null)
            {
                node.Attributes["password"].InnerText = node.Attributes["newPassword"].InnerText;
                node.Attributes["newPassword"].InnerText = string.Empty;
            }

            doc.Save(path);
        }

        private void GenerateNewPassword()
        {
            byte[] random = new byte[10];

            _randomizer.NextBytes(random);

            _pwd = Convert.ToBase64String(random) + "@Ax";

            _pwd = RijndaelCryptographer.EncryptString(_pwd);

            XmlDocument doc = new XmlDocument();

            string path = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).FilePath;

            doc.Load(path);

            XmlNode node = doc.SelectSingleNode(string.Format("/configuration/fixConfiguration/fixSessions/session[@name='{0}']", _sessionInformation.Name));

            if (node != null && node.Attributes["newPassword"] == null)
            {
                XmlAttribute attr;
                attr = doc.CreateAttribute("newPassword");
                attr.Value = _pwd;
                node.Attributes.Append(attr);
            }

            doc.Save(path);
        }

        #endregion

        #region IsMessageOfType

        private bool IsMessageOfType(QuickFix.Message message, String type)
        {
            try
            {
                return type.Equals(message.getHeader().getField(new MsgType()).getValue());
            }
            catch (FieldNotFound ex)
            {
                Logger.Write(ex);
                return false;
            }
        }

        #endregion

        #region SaveMessage

        public void SaveMessage(QuickFix44.ExecutionReport message, QuickFix.SessionID sessionID)
        {
            this.SaveMessage(message, sessionID, MessageType.ExecutionReport);
        }

        public void SaveMessage(QuickFix44.Message message, QuickFix.SessionID sessionID, string messageType)
        {
            //aguardar a aplicacao entrar em modo normal de operacao
            if (_normalMode.WaitOne())
            {
                int seqNum = Convert.ToInt32(message.getHeader().getField(34));
                DateTime receiveDate = DateTime.MinValue;

                DateTime.TryParseExact(message.getHeader().getField(52).ToString(), "yyyyMMdd-HH:mm:ss.fff", null, System.Globalization.DateTimeStyles.None, out receiveDate);

                int companyId = -1;
                if (message.isSetField(453))
                {
                    int partiesCount = Convert.ToInt32(message.getField(453));
                    for (uint i = 1; i <= partiesCount; i++)
                    {
                        NoPartyIDEntity partyModel = new NoPartyIDEntity();
                        QuickFix.Group group = new QuickFix.Group(453, 448);
                        message.getGroup(i, group);
                        partyModel.PartyRole = Convert.ToInt32(group.getField(452));
                        if (partyModel.PartyRole == 7)
                            companyId = Convert.ToInt32(group.getField(448));
                    }
                }

                lock (_messagesToDelivery)
                {
                    _messagesToDelivery.Enqueue(new RepositoryParameter() { Id = seqNum, CompanyId = companyId, ReceiveDate = receiveDate, Message = message.ToString(), MessageType = messageType });
                }
            }
        }

        #endregion

        #region DeliveryMessages

        //private void DeliveryRepositoryMessages(object state)
        private void DeliveryRepositoryMessages()
        {
            //while (!System.Diagnostics.Debugger.IsAttached)
            //    Thread.Sleep(1);

            //somente entrara no loop caso a aplicacao esteja em modo normal de operacao
            while (!_exitDeliveryBackupMessages.WaitOne(_sessionInformation.Period) && _normalMode.WaitOne())
            {
                RepositoryParameter[] parameters = null;

                lock (_messagesToDelivery)
                {
                    try
                    {
                        if (_messagesToDelivery.Count > 0)
                        {
                            parameters = _messagesToDelivery.ToArray();

                            IRepositoryProvider repository = RepositoryManager.GetRepositoryProvider();

                            //se o numero maximo de mensagens por bloco for maior ou igual ao numero de mensagens
                            //a entregar, ou o tamanho do bloco seja um numero menor ou igual a zero, entregar todas as mensagens da fila 
                            //em uma única operação
                            if (!repository.MaxLinesPerBlock.HasValue || repository.MaxLinesPerBlock >= _messagesToDelivery.Count)
                            {
                                repository.Save(parameters);
                                _messagesToDelivery.Clear();
                            }
                            //caso o tamanho do bloco seja menor que o total de mensagens a entregar,
                            //entregar em blocos
                            else
                            {
                                int blockSize = 0;
                                int laps = 0;

                                //enquanto houver mensagens a entregar
                                while (_messagesToDelivery.Count > 0)
                                {
                                    laps++;
                                    if (laps == 100)
                                    {
                                        Thread.Sleep(1);//para manter baixo o consumo do processador
                                        laps = 0;
                                    }

                                    RepositoryParameter[] parametersBlock;

                                    if (repository.MaxLinesPerBlock >= _messagesToDelivery.Count)
                                        blockSize = _messagesToDelivery.Count;
                                    else
                                        blockSize = repository.MaxLinesPerBlock.GetValueOrDefault();

                                    //criar o array para armazenar o bloco de mensagens
                                    parametersBlock = new RepositoryParameter[blockSize];

                                    //montar um bloco de mensagens
                                    for (int i = 0; i < blockSize; i++)
                                    {
                                        parametersBlock[i] = parameters[i];
                                    }

                                    //salvar o bloco de mensagens
                                    repository.Save(parametersBlock);

                                    //remover o bloco de mensagens da fila de mensagens a entregar
                                    for (int i = 0; i < parametersBlock.Length; i++)
                                        _messagesToDelivery.Dequeue();

                                    parameters = _messagesToDelivery.ToArray();
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Write(ex);

                        if (parameters != null)
                        {
                            lock (_fileRepository)
                            {
                                _fileRepository.SaveData(_messagesToDelivery.ToArray());
                            }

                            _messagesToDelivery.Clear();
                        }

                        //setar a aplicação para modo de contingencia e parar de receber mensagens do FIX
                        _normalMode.Reset();
                    }
                }
            }
        }

        private void DeliveryBackupMessages()
        {
            //while (!System.Diagnostics.Debugger.IsAttached)
            //    Thread.Sleep(1);

            int laps = 0;

            while (!_exitDeliveryBackupMessages.WaitOne(0))
            {
                //codigo utilizado para manter baixo o consumo de processador
                //->
                laps++;
                if (laps == 100)
                {
                    Thread.Sleep(1);
                    laps = 0;
                }
                //<-

                //se a aplicacao nao estiver em modo normal de operacao(modo de contingencia)
                if (!_normalMode.WaitOne(0))
                {
                    try
                    {
                        lock (_fileRepository)
                        {
                            //se houver algum arquivo
                            if (_fileRepository.Count > 0)
                            {
                                IRepositoryProvider repository = RepositoryManager.GetRepositoryProvider();

                                //se não for informado um tamanho maximo de block, salvar todos os dados
                                if (!repository.MaxLinesPerBlock.HasValue)
                                {
                                    repository.Save(_fileRepository.GetData());
                                }
                                else
                                {
                                    int blockSize;
                                    int laps1 = 0;

                                    //recuperar os dados do arquivo
                                    RepositoryParameter[] parameters = _fileRepository.GetData();

                                    while (parameters.Length > 0)
                                    {
                                        //codigo utilizado para manter baixo o consumo de processador
                                        //->
                                        laps1++;
                                        if (laps1 == 100)
                                        {
                                            Thread.Sleep(1);
                                            laps1 = 0;
                                        }
                                        //<-

                                        //definir o tamanho maximo do bloco
                                        if (repository.MaxLinesPerBlock > parameters.Length)
                                            blockSize = parameters.Length;
                                        else
                                            blockSize = repository.MaxLinesPerBlock.GetValueOrDefault();

                                        //recuperar o bloco de linhas a tratar
                                        RepositoryParameter[] parametersBlock = parameters.Take(blockSize).ToArray<RepositoryParameter>();

                                        //salvar o bloco de mensagens
                                        repository.Save(parametersBlock);

                                        //remover os itens salvos no repositorio
                                        parameters = parameters.Skip(blockSize).ToArray<RepositoryParameter>();

                                        //atualizar o arquivo com os itens restantes
                                        _fileRepository.UpdateData(parameters);
                                    }
                                }

                                //caso consiga salvar todos os dados, remover o arquivo
                                _fileRepository.DeleteFile();
                            }
                        }

                        //caso consiga salvar os dados dos arquivos
                        //setar a aplicacao para o modo normal de operacao e voltar a receber mensagens do FIX
                        _normalMode.Set();
                    }
                    catch (Exception ex)
                    {
                        Logger.Write(ex);

                        Thread.Sleep(5000);
                    }
                }
            }
        }

        #endregion

        #region Dispose

        public void Dispose()
        {
            if (_exitDeliveryBackupMessages != null)
                _exitDeliveryBackupMessages.Set();

            if (_exitDeliveryRepositoryMessages != null)
                _exitDeliveryRepositoryMessages.Set();

            if (_socketInitiator != null)
                _socketInitiator.Dispose();
        }

        #endregion
    }
}
